/**
  Comun backend for comun transpiler.

  by drummyfish, released under CC0 1.0, public domain
*/

const char *_noPopSymbol(const uint8_t *instr)
{
  return (instr[1] & CMN_MASK_INSTR_NOPOP) ? "'" : "";
}

uint8_t _isIncOrDec(const uint8_t *instr)
{
  return (instr[0] == CMN_OPCODE_ADC || instr[0] == CMN_OPCODE_SUC) &&
    !(instr[1] & CMN_MASK_INSTR_CON) && ((instr[1] & 0x0f) == 1);
}

int _typeEnvToNum(uint8_t typeEnv)
{
  return (typeEnv + (typeEnv == 3)) * 8;
}

uint8_t printBytecodeAsComun(const uint8_t *bytecode, FILE *f, const CMN_Compiler *compiler)
{
  const uint8_t *instr;
  uint8_t currentTypeEnv = 0;
  uint32_t n = 0;
  const char is[] = "                                ";
  const char *indentStr = is + 32;
  uint32_t pCounts[4],tmp[4];

  CMN_estimateMemory(bytecode,0,tmp,pCounts);

  for (int8_t i = 3; i >= 0; --i)
  {
    if (pCounts[i] <= 1)
      continue;

    fprintf(f,"~%d\n",_typeEnvToNum(i));
   
    uint32_t pSizes[pCounts[i]];

    for (uint16_t j = 0; j < pCounts[i]; ++j)
      pSizes[j] = 0;

    instr = bytecode + CMN_BYTECODE_HEADER_SIZE;

    uint32_t previousAddr = 0;

    while (*instr != CMN_OPCODE_END)
    {
      if (instr[0] == CMN_OPCODE_PSC && (CMN_instrTypeEnv(instr)) == i)
      {
        uint64_t c1, c2;
        CMN_instrGetConsts(instr,&c1,&c2);
 
        if (c1 > CMN_LAST_SPECIAL_PTR)
        {
          c1 -= CMN_LAST_SPECIAL_PTR + 1;
          pSizes[c1] = 1;
          c1--;
        }
        else
          c1 = pCounts[i] - 2;
               
        if (c2 != 0)
        {
          while (pSizes[c1] != 1)
            c1--;

          pSizes[c1] = c2 - previousAddr;
          previousAddr = c2;
        }
      }

      instr += 2;
    }

    for (uint8_t j = 0; j < pCounts[i] - 1; ++j)
    {
      fprintf(f,"  ~%s",pointerIndexToName(compiler,
        CMN_LAST_SPECIAL_PTR + j + 1,i));

      if (pSizes[j] != 1)
        fprintf(f,":%d",pSizes[j]);

      fputc('\n',f);
    }
  }

  instr = bytecode + CMN_BYTECODE_HEADER_SIZE;

  while (*instr != CMN_OPCODE_END)
  {
    if (*instr >= CMN_OPCODE_SPECIALS && !_isIncOrDec(instr) &&
      (*instr & CMN_MASK_INSTR_MODE) == CMN_OPCODE_1C1)
    {
      fputs("~tmp ~8 ~tmp ~16 ~tmp ~32 ~tmp ~0\n\n",f);
      break;
    }

    instr += 2;
  }

  instr = bytecode + CMN_BYTECODE_HEADER_SIZE;

  while (*instr != CMN_OPCODE_END) // main pass
  {
    uint8_t env = CMN_instrTypeEnv(instr);

    if ((CMN_instrTouchesMem(*instr) || CMN_instrTouchesPtr(*instr) ||
      (*instr == CMN_OPCODE_DES &&
        (instr[1] == CMN_DES_IF || instr[1] == CMN_DES_LOOP)))
      && env != currentTypeEnv && *instr != CMN_OPCODE_PSC)
    {
      fprintf(f,"~%d\n",_typeEnvToNum(env));
      currentTypeEnv = env;
    }

    if (*instr >= CMN_OPCODE_SPECIALS)
    {
      // first check some common patterns, like "ADC 1" is really ++
      if (_isIncOrDec(instr))
      {
        fprintf(f,instr[0] == CMN_OPCODE_ADC ? "%s++%s\n" : "%s--%s\n",
          indentStr,_noPopSymbol(instr));
      }
      else
      {
        const char *opStr = "#?#";

        switch (*instr & CMN_MASK_INSTR_GROUP)
        {
          case CMN_OPCODE_AD: opStr = "+"; break;
          case CMN_OPCODE_SU: opStr = "-"; break;
          case CMN_OPCODE_MU: opStr = "*"; break;
          case CMN_OPCODE_DI: opStr = "/"; break;
          case CMN_OPCODE_DS: opStr = "//"; break;
          case CMN_OPCODE_MO: opStr = "%"; break;
          case CMN_OPCODE_MS: opStr = "%%"; break;

          case CMN_OPCODE_EQ: opStr = "="; break;
          case CMN_OPCODE_NE: opStr = "!="; break;

          case CMN_OPCODE_GR: opStr = ">"; break;
          case CMN_OPCODE_GE: opStr = ">="; break;
          case CMN_OPCODE_SM: opStr = "<"; break;
          case CMN_OPCODE_SE: opStr = "<="; break;

          case CMN_OPCODE_GS: opStr = ">>"; break;
          case CMN_OPCODE_BS: opStr = ">>="; break;
          case CMN_OPCODE_SS: opStr = "<<"; break;
          case CMN_OPCODE_LS: opStr = "<<="; break;

          case CMN_OPCODE_BA: opStr = "&"; break;
          case CMN_OPCODE_BO: opStr = "|"; break;
          case CMN_OPCODE_BX: opStr = "|!"; break;

          case CMN_OPCODE_LA: opStr = "&&"; break;
          case CMN_OPCODE_LO: opStr = "||"; break;
          case CMN_OPCODE_LX: opStr = "|!!"; break;

          case CMN_OPCODE_SR: opStr = "|>"; break;
          case CMN_OPCODE_SL: opStr = "|<"; break;

          case (CMN_OPCODE_BNO & CMN_MASK_INSTR_GROUP): opStr = "!"; break;
          case (CMN_OPCODE_OUT & CMN_MASK_INSTR_GROUP): opStr = "->"; break;
          case (CMN_OPCODE_INU & CMN_MASK_INSTR_GROUP): opStr = "<?"; break;
          case (CMN_OPCODE_INP & CMN_MASK_INSTR_GROUP): opStr = "<-"; break;
          case (CMN_OPCODE_ADR & CMN_MASK_INSTR_GROUP): opStr = "$$"; break;

          default: break;
        }

        if ((*instr & CMN_MASK_INSTR_MODE) == CMN_OPCODE_1C1)
        {
          uint64_t c = CMN_instrGetConst(instr);

          fprintf(f,"%s$>0 $:tmp ",indentStr);

          if (currentTypeEnv != 0 || c < 0x7fffffff)
            fprintf(f,"%lu ",c);
          else
            fprintf(f,"%d ",(((int) c) - 0xffffffff) - 1);

          fprintf(f,"%s%s $tmp ^\n",opStr,_noPopSymbol(instr));
        }
        else
          fprintf(f,"%s%s%s\n",indentStr,opStr,_noPopSymbol(instr));
      }
    }
    else // special instructions
      switch (*instr)
      {
        case CMN_OPCODE_DES:
          switch (instr[1])
          {
            case CMN_DES_FUNC:
              fprintf(f,"%s:\n",functionIndexToName(compiler,
                bytecodeGetBlock(bytecode,n) - 1,0));
              indentStr -= 2;
              break;
 
            case CMN_DES_GOTO:
              fprintf(f,">l%lu\n",CMN_instrGetConst(instr + 2));
              break;

            case CMN_DES_LABEL:
              fprintf(f,"~:l%u\n",n + 1);
              break;

            case CMN_DES_IF:
              fprintf(f,"%s?%s\n",indentStr,
                ((instr[2] == CMN_OPCODE_JIA || instr[2] == CMN_OPCODE_JNA)
                && (instr[3] & CMN_MASK_INSTR_NOPOP)) ? "'" : "");

              indentStr -= 2;
              break;

            case CMN_DES_LOOP:
            {
              uint8_t notInfinite = (instr[2] == CMN_OPCODE_JIA) ||
                (instr[2] == CMN_OPCODE_JNA);

              fprintf(f,"%s@%s%s\n",indentStr,notInfinite ? "" : "@",
                notInfinite && (instr[3] & CMN_MASK_INSTR_NOPOP) ? "'" : "");

              indentStr -= 2;
              break;
            }

            case CMN_DES_LOOP_BREAK:
              fprintf(f,"%s!@\n",indentStr);
              break;

            case CMN_DES_EXIT:
              fprintf(f,"%s!.\n",indentStr);
              break;

            case CMN_DES_ELSE:
              fprintf(f,"%s;\n",indentStr + 2);
              break;

            case CMN_DES_LOOP_END:
            case CMN_DES_IF_END: 
              indentStr += 2;
              fprintf(f,"%s.\n",indentStr);
              break;

            default: break;
          }
          break;

        case CMN_OPCODE_CAL:
          fprintf(f,"%s%s\n",indentStr,
            functionIndexToName(compiler,bytecodeGetBlock(bytecode,
              CMN_instrGetConst(instr)) - 1,0));
          break;

        case CMN_OPCODE_CAE:
          fprintf(f,"%s%s\n",indentStr,
            functionIndexToName(compiler,CMN_instrGetConst(instr),1));
          break;

        case CMN_OPCODE_CON:
        {
          uint64_t strLiteralLen = instructionsAreStringLit(instr,5);

          if (strLiteralLen)
          {
            fprintf(f,"%s\"",indentStr);

            instr += 4 * (strLiteralLen - 1);

            for (int i = 0; i < ((int) strLiteralLen); ++i)
            {
              fputc(CMN_instrGetConst(instr),f);
              instr -= 4;
            }

            instr += 4 * strLiteralLen;
            n += 2 * (strLiteralLen - 1);
            fputs("\"\n",f);
          }
          else
          {
            fprintf(f,"%s%lu",indentStr,CMN_instrGetConst(instr));

            if (!(instr[1] & CMN_MASK_INSTR_NOPOP))
              fputs(" ^",f);

            fputc('\n',f);
          }

          break;
        }

        case CMN_OPCODE_CND:
          fprintf(f,"%s??%s\n",indentStr,_noPopSymbol(instr));
          break;

        case CMN_OPCODE_SWP:
          fprintf(f,"%s><%s\n",indentStr,_noPopSymbol(instr));
          break;

        case CMN_OPCODE_MGE:
        case CMN_OPCODE_MEX:
          fprintf(f,"%s$%s%s%s\n",indentStr,instr[0] == CMN_OPCODE_MEX ?
            ":" : "",pointerIndexToName(compiler,CMN_instrGetConst(instr),
            currentTypeEnv),_noPopSymbol(instr));
          break;

        case CMN_OPCODE_PAC:
        {
          uint64_t c1, c2;
          uint8_t negative = 0;
          CMN_instrGetConsts(instr,&c1,&c2);

          if (c2 >= 0x08)
          {
            c2 = 16 - c2;
            negative = 1;
          }
 
          while (c2 != 0)
          {
            fprintf(f,"%s$%c%s\n",indentStr,negative ? '<' : '>',
              pointerIndexToName(compiler,c1,currentTypeEnv));
            c2--;
          }

          break;
        }

        case CMN_OPCODE_PUX:
          fprintf(f,"%s$%s\n",indentStr,_noPopSymbol(instr));
          break;

        case CMN_OPCODE_PAX:
          fprintf(f,"%s$+%s%s\n",indentStr,pointerIndexToName(compiler,
            CMN_instrGetConst(instr),currentTypeEnv),_noPopSymbol(instr));
          break;

        case CMN_OPCODE_PCO:
        case CMN_OPCODE_PCM:
        {
          uint64_t c1, c2;
          CMN_instrGetConsts(instr,&c1,&c2);
          fprintf(f,"%s$%s%c",indentStr,pointerIndexToName(compiler,c2,
            currentTypeEnv),instr[0] == CMN_OPCODE_PCO ? '>' : '=');
          fprintf(f,"%s\n",pointerIndexToName(compiler,c1,currentTypeEnv));
          break;
        }

        case CMN_OPCODE_RET:
          fputs(".\n\n",f);
          indentStr += 2;
          break;

        case CMN_OPCODE_TRA:
          fprintf(f,"%s>%d\n",indentStr,
            _typeEnvToNum(CMN_instrGetConst(instr)));
          break;

        case CMN_OPCODE_POP:
          fprintf(f,"%s^",indentStr);

          for (int i = 0; i < ((int) CMN_instrGetConst(instr)); ++i)
          {
            fputc(' ',f);
            fputc('^',f);
          }

          fputc('\n',f);
          break;

        default: break;
      }

    instr += 2;
    n++;
  }

  return 1;
}
